Skip to content

feat: sanitize rich clipboard HTML - #65

Draft
seonghobae wants to merge 86 commits into
mainfrom
feat/safe-rich-clipboard
Draft

feat: sanitize rich clipboard HTML#65
seonghobae wants to merge 86 commits into
mainfrom
feat/safe-rich-clipboard

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Purpose

Add a bounded, fail-closed rich-HTML paste boundary for Microsoft Word, Google Docs, email, and web content before ProseMirror parses or inserts it.

Current implementation

  • reconstruct pasted HTML into a strict semantic allowlist from an inert template;
  • preserve headings, paragraphs, lists, tables, blockquotes, code, semantic emphasis, and existing SafeLink-approved hyperlinks;
  • convert only bold, italic/oblique, underline, and line-through inline styles to semantic elements;
  • drop scripts, styles, metadata including complete title subtrees, forms, embedded content, SVG/MathML, media, hidden subtrees, comments, remote/local images, event handlers, IDs, classes, proprietary attributes, and arbitrary styles;
  • treat display: none, visibility: hidden, visibility: collapse, hidden, aria-hidden="true", and exact Office mso-hide: all as complete hidden-subtree boundaries;
  • preserve only the rendered first summary from closed details, drop closed dialog subtrees, and sanitize rendered contents of open variants;
  • drop complete progress, meter, datalist, noframes, and noembed subtrees so native-widget, hidden-suggestion, or obsolete fallback text cannot become ordinary editor prose after wrapper removal;
  • remove closed and EOF-terminated CSS comments and decode bounded CSS escapes for exact Office mso-hide: all matching while retaining malformed and look-alike declarations as visible text;
  • classify each anchor URL once through the canonical SafeLink policy and reuse that exact decision for wrapper creation and attribute copying;
  • bound UTF-8 source bytes, traversed nodes, and depth with iterative traversal;
  • expose stable redacted error codes and latest-callback wiring;
  • apply the same extension to standalone and provider-neutral Yjs editors; and
  • introduce no runtime dependency, network, storage, model, credential, database, scheduler, or reviewer-chain change.

Test-first hardening

  • RED dac8fef84baa313d9cada2e1c3c2bbf658b7675d / GREEN e6d51c9b3fdaffc707d11649438aa9dcdd9f932e: consume a final CSS comment through EOF so mso-hide: all/* cannot surface hidden text;
  • RED c11617099f03cf691dfa5d7c116650faf5ad2b40 / GREEN d9fcc0f1da56e702b4efcb1e679cba70f2fb5efb: discard title metadata subtrees instead of unwrapping metadata text into the editor;
  • RED eebf18a623b7702e55995c4d406fe07203edfd39 / GREEN e84261e74dcb07ed6afec9a934adcf5b8b1e41e3: prevent closed disclosure and dialog source-only text from becoming visible editor content while retaining rendered summary/open content;
  • RED 6e6d48ff8a377bab875d6a520580e65d3489f78e / GREEN 01683cc89faadfcacf05f358e34b6a1812e61e77: prevent progress, meter, noframes, and noembed fallback descendants from surfacing after wrapper removal;
  • RED a48d81eac541effea5d7742a47c0ca6bd01d04ed / GREEN ec5d602bcf4f8e82fcc3136a82e354816913f6df: prevent hidden datalist suggestions and down-level fallback descendants from surfacing as ordinary editor prose;
  • RED 6dadce2459594d1721468086c8c60b8605ce0f52 / GREEN 00d163eecffafa0b4c6f108f45bc4af39273c41e: prove one safe anchor href is read and classified exactly once and reuse the validated value without a second policy evaluation;
  • RED 77ea951c54230f896788157913a924186ae487f7 / GREEN 503ba7f74001c2bfc05bee76d154fc7b162200f3: prevent visibility: collapse table rows, cells, and ordinary subtrees from exposing non-rendered source text while retaining visible siblings; and
  • added a deterministic 3,000-paragraph Word-like capacity fixture that measures 9,000 traversed source nodes, proves it remains below the 10,000-node default, preserves all paragraph/emphasis semantics, and removes source class/style attributes.

Current post-prerequisite finding

Exact-head review of 95dc3eb8a7d40f049859d94a8019c594607db612 identified one valid hidden-content gap: content-visibility: hidden is not recognized, so source-only descendants can be unwrapped into ordinary editor prose after the source style attribute is discarded.

The current inline thread remains intentionally unresolved. To prevent competing branch writers and preserve dependency order, this branch must not be modified until PR #64 reaches main. On the exact integrated base, the repair must be test-first and cover ordinary, case, whitespace, terminal !important, CSS-comment, and CSS-escaped forms plus false positives such as visible, auto, hiddenly, and prefixed property names. The implementation must use the bounded raw-style hidden-declaration boundary rather than depend on engine-specific CSSOM exposure, then update operator guidance, APA 7 doctoring, documentation contracts, and CHANGELOG.md.

Review and documentation reconciliation

  • addressed every earlier actionable CodeRabbit finding and left stale, superseded, or incorrect findings closed;
  • added docs/doctoring/visibility-collapse-hidden-content.md with W3C CSS visibility/collapse rationale, APA 7th references, test evidence, rollback, and compatibility boundaries;
  • bound that decision to a deterministic repository-document contract test;
  • retained the explicit jsdom-versus-cross-engine assurance boundary and the Chromium, Firefox, and WebKit differential release gate;
  • resolved earlier inline review threads only after their addressed revisions passed exact-head verification; and
  • retained the new content-visibility: hidden finding as unresolved until the post-docs: define Inkspan modular acquisition architecture #64 integrated fix passes every gate.

Unresolved inline review threads: 1.

Exact-head verification

Verified code head: 95dc3eb8a7d40f049859d94a8019c594607db612.

  • CI run 31058980885: passed TypeScript typecheck, 100% production statement/branch/function/line coverage, deterministic builds, packed ESM/CommonJS/strict-TypeScript consumers, SSR-safe imports, demo build, and Office Python 3.11/3.14 dependency/docstring/100%-branch-coverage/wheel/package gates;
  • Security Scan run 31058980977: passed;
  • SAST Semgrep run 31058980840: passed; and
  • CodeRabbit combined status: passed.

These successful code-head checks predate the newly recorded valid review finding and do not establish merge readiness. The predecessor RED head 77ea951c54230f896788157913a924186ae487f7 failed CI run 31058675380 as intended before the production repair; its successful security runs are retained only as test-first history and are not used as merge evidence.

Standards and assurance boundary

The design and doctoring records cite the official W3C Clipboard API and Events Working Draft, WHATWG HTML parsing/rendering semantics, W3C CSS visibility and table-collapse semantics, W3C CSS Syntax Module Level 3 comment consumption, OWASP guidance, and TipTap/ProseMirror paste-transform contracts in APA 7th style. The post-prerequisite repair must add the W3C CSS Containment Module Level 2 content-visibility decision and its claim boundary. Current jsdom evidence is not represented as cross-engine browser conformance; a dependency-locked Chromium, Firefox, and WebKit differential corpus remains a publication gate for 0.6.0.

Dependency and integration order

PR #64 is the authoritative standalone and modular acquisition-architecture prerequisite. Both branches currently add ARCHITECTURE.md and modify CHANGELOG.md from the same base, so merging or writing them in parallel would create competing ownership and an avoidable integration conflict.

Required order:

  1. independently approve and merge PR docs: define Inkspan modular acquisition architecture #64 without bypass;
  2. update this branch once to the exact integrated main head, preserve docs: define Inkspan modular acquisition architecture #64's authoritative architecture contract, and reconcile only the SafeClipboard-specific architecture and changelog additions;
  3. add the failing content-visibility: hidden regression before the implementation repair, update authoritative documentation and doctoring, and restore 100% statement/branch/function/line coverage;
  4. rerun every required exact-head CI, security, coverage, packaging, Strix, CodeRabbit, OpenCode, Noema, and other policy gate; and
  5. obtain a new qualifying non-author approval for the integrated exact head before merge.

This PR remains Draft until PR #64 is integrated, the valid hidden-content finding is addressed, and the exact post-integration head is revalidated. No parallel repair agent or workflow may write either branch.

Remaining merge gates

Do not merge until:

  1. PR docs: define Inkspan modular acquisition architecture #64 is integrated in the dependency order above;
  2. the content-visibility: hidden finding is fixed test-first and its thread is resolved only after exact-head verification;
  3. CodeRabbit, OpenCode, Noema, Strix, GHAS, Dependabot, human, and all other required review surfaces complete on the resulting integrated exact head without an infrastructure-only result;
  4. a qualifying non-author independently approves that exact head;
  5. zero valid unresolved findings remain; and
  6. branch protection and repository policy report every required context satisfied.

The current-head CodeRabbit read-only comment found no actionable source-level issue, but it is not a formal approval and does not supersede the separately identified unresolved hidden-content finding. The default paste behavior change targets 0.6.0 only through a separate verified release PR.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

SafeClipboard가 리치 HTML 클립보드를 allowlist 기반으로 정제하고, 제한 초과·설정·DOM 오류를 비공개 오류로 처리합니다. standalone 및 협업형 편집기가 이를 공유 확장으로 사용합니다. 관련 문서와 테스트, CI 실행 환경 및 checkout 보안 설정도 추가했습니다.

Changes

SafeClipboard 리치 클립보드 정제

Layer / File(s) Summary
클립보드 계약과 보안 정책
ARCHITECTURE.md, CHANGELOG.md, docs/clipboard-security.md, docs/superpowers/specs/...
허용 구조, 위험 콘텐츠 제거, 입력 한계, 오류 처리, 변환 순서, 호스트 책임 및 릴리스 조건을 정의했습니다.
HTML 정제 구현
src/extensions/SafeClipboard.ts
HTML을 detached document에서 재구성하고, 숨김·활성·리소스 콘텐츠를 제거합니다. 안전한 링크와 제한된 의미 스타일을 보존하며 실패 시 빈 결과와 안정적인 오류 코드를 반환합니다.
확장 키트와 편집기 통합
src/extensions/kit.ts, src/index.ts, src/types.ts, src/components/CwlEditor.tsx, src/collaboration/CollaborativeCwlEditor.tsx
SafeClipboard를 기본 확장 집합과 공개 API에 추가했습니다. 두 편집기에서 clipboard 설정과 최신 오류 콜백을 전달합니다.
정제 및 통합 검증
src/extensions/*test*, src/components/CwlEditor.clipboard.test.tsx, src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx, src/clipboardDocumentation.test.ts
정제 규칙, Office 숨김 처리, 대화형 콘텐츠, 제한 초과, DOM 부재, 오류 비공개, 변환 순서 및 콜백 수명을 검증합니다.
운영 문서와 구현 계획
docs/doctoring/*, docs/superpowers/plans/*
SafeClipboard의 세부 결정, 테스트 범위, 브라우저 검증 한계, 유지보수와 0.6.0 릴리스 절차를 기록했습니다.

CI 실행 및 checkout 보안 강화

Layer / File(s) Summary
CI 실행 계약과 검증
.github/workflows/ci.yml, src/workflowExactHead.test.ts
작업을 Ubuntu 24.04에서 실행하고 checkout action을 갱신했습니다. PR head SHA 또는 현재 SHA를 checkout하며 자격 증명 저장을 비활성화합니다. 관련 권한과 해시 고정 조건을 테스트합니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Clipboard
  participant CwlEditor
  participant buildExtensions
  participant SafeClipboard
  participant TipTap
  Clipboard->>CwlEditor: text/html 붙여넣기
  CwlEditor->>buildExtensions: clipboard 설정 및 오류 콜백 전달
  buildExtensions->>SafeClipboard: SafeClipboard 등록
  SafeClipboard->>SafeClipboard: HTML 제한 및 allowlist 재구성
  SafeClipboard->>TipTap: 정제 HTML 또는 빈 문자열 전달
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 리치 클립보드 HTML 정제라는 변경 사항의 핵심을 정확하고 간결하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/safe-rich-clipboard

Comment @coderabbitai help to get the list of available commands.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head finding (valid, blocking Draft→Ready): the standalone/collaborative integration bypasses the sanitizer's fail-closed ClipboardConfig validation. buildExtensions() dereferences clipboard.maxHtmlBytes, maxNodes, and maxDepth before sanitizeRichClipboardHtml() runs, then constructs a fresh known-key object. As a result, an accessor/proxy configuration can execute or throw during editor construction, and unknown/symbol/non-enumerable configuration keys are silently discarded rather than producing the documented redacted invalid_configuration error on rich paste. This contradicts docs/clipboard-security.md (“Invalid configuration fails closed when rich HTML is pasted”) and the direct sanitizer contract.

Please fix test-first without weakening the current direct API: add a regression proving buildExtensions()/both React surfaces do not evaluate nested accessors at construction, preserve the original config object into SafeClipboard, and return '' plus one redacted invalid_configuration callback on paste. A bounded option such as config?: ClipboardConfig may coexist with the current direct numeric extension options; the transform should validate the preserved config at paste time. Keep callback liveness, SSR import safety, and 100% statement/branch/function/line coverage.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head CI triage for 26cd91f53d97a6897b3dc74753d7a67ef21750ea:

  1. Valid implementation failure: mso-hide: all survives because CSSStyleDeclaration.getPropertyValue('mso-hide') drops/does not expose the proprietary declaration in jsdom (and cannot be relied on uniformly across engines). Inspect the raw style attribute without executing anything, parse bounded declarations, and drop the subtree when an exact case-insensitive mso-hide: all declaration is present. Keep ordinary style attributes discarded from output and add variants for whitespace, case, !important, CSS comments, and misleading values.

  2. Incorrect assertion: expect(sanitized).not.toMatch(/section|font/i) necessarily fails because the same test explicitly requires preserving visible text font text. Parse the sanitized fragment and assert querySelector('section, font') === null while retaining font text; do not weaken the intended visible-text preservation.

The earlier blocking integration finding remains unresolved: buildExtensions() still dereferences and reconstructs ClipboardConfig, bypassing accessor/unknown-key fail-closed validation on both React surfaces. Address all three issues test-first before moving Draft→Ready.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional exact-head standards/security findings for a1c952e7124e7fb37e6ff7f5efada756c24cb93f:

  1. Invalid/nonexistent standards citation: docs/doctoring/safe-rich-clipboard.md cites a “W3C Working Draft dated 24 June 2026” and https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/. The official current W3C TR page and publication history identify the latest published Working Draft as 24 November 2025 (https://www.w3.org/TR/clipboard-apis/, https://www.w3.org/standards/history/clipboard-apis/). Replace the nonexistent date/version with the current official publication, preserve the work-in-progress claim boundary, and add a deterministic documentation assertion so a future fabricated dated URL cannot recur.

  2. Paste-transform ordering is not yet defended: current TipTap documentation states that extension transformPastedHTML hooks are chained by extension priority, with higher priority running first and each later transform receiving the prior output (https://tiptap.dev/docs/editor/extensions/custom-extensions/create-new/extension). SafeClipboard currently uses default priority. A subsequent transform can therefore reintroduce resource-bearing or executable markup before ProseMirror parses it, contradicting the stated complete pre-parse trust boundary for modular composition. Set and document an explicit lowest-practical sanitizer priority (or otherwise make it the final transform), and add an integration test with competing transforms proving final pre-parse output is re-sanitized. Clearly document the residual boundary for a hostile host that deliberately installs a later transform.

  3. Browser-realistic assurance is missing for a bespoke sanitizer: current acceptance uses jsdom only, while the implementation depends on HTML fragment parsing, CSS declaration exposure, inertness, and serialization behavior. The first exact-head failure already demonstrated a jsdom/CSSOM mismatch for mso-hide. Add deterministic cross-engine browser tests (Chromium, Firefox, and WebKit where supported) for the security corpus and output parity, or explicitly narrow the conformance claim and record the residual acquisition risk. OWASP currently recommends a maintained HTML sanitizer such as DOMPurify and regular patching; if the no-runtime-dependency bespoke reconstruction remains the decision, the doctoring record must explain the compensating fuzz/differential-testing and vulnerability-response obligations rather than implying OWASP directly endorses this implementation.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to my review 4865041574: withdraw and supersede finding 1 about the W3C publication date. Direct verification of the official dated W3C publication confirms that https://www.w3.org/TR/2026/WD-clipboard-apis-20260624/ is the genuine Clipboard API and Events — W3C Working Draft, 24 June 2026. The existing doctoring citation is therefore valid on that basis and does not require replacement. I regret the earlier false finding, which resulted from relying on a stale publication-history search result rather than opening the official dated document.

The other findings in that review remain open unless subsequent commits address them: deterministic final-transform ordering under TipTap extension composition, and browser-realistic cross-engine assurance/explicit residual-risk documentation for the bespoke sanitizer.

The earlier hostile-config integration finding is now addressed at this exact head: buildExtensions() preserves the original ClipboardConfig object without dereferencing it, and validation remains deferred to paste time. This acknowledgement does not mark any unrelated finding resolved.

Copy link
Copy Markdown
Contributor Author

Exact-current-head review request for 75e96ebc2a853f9315eb166b8eb98264109b238e against base 66cf4b9e46b5cbbc936ba470da5d1e96ca1649af.

CI 31047486712, Security Scan 31047486296, and SAST Semgrep 31047487208 completed successfully on this exact head. Production statement, branch, function, and line coverage remain 100%; packed ESM/CommonJS/strict-TypeScript consumers, SSR-safe imports, demo build, and Office Python 3.11/3.14 gates passed; and there are zero unresolved inline threads.

This cycle added a test-first fail-closed boundary for native progress/meter and obsolete noframes/noembed fallback descendants, plus operator guidance, APA 7th doctoring, deterministic documentation assertions, and changelog evidence. Review this exact head read-only and distinguish substantive findings from stale-head, duplicate, author-only, wrapper-only, or infrastructure-only results.

@coderabbitai review
@opencode-agent review
@cwl-noema-review review

A qualifying non-author independent approval and every branch-protection gate remain mandatory. Do not merge, update the branch, alter reviewer identities, or treat queued/pending/rate-limited evidence as success.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='ContextualWisdomLab/inkspan'
pr='65'
base='66cf4b9e46b5cbbc936ba470da5d1e96ca1649af'
head='75e96ebc2a853f9315eb166b8eb98264109b238e'

printf '%s\n' '== Revision verification =='
printf 'checked-out='; git rev-parse HEAD
printf 'requested='; git rev-parse "$head"
printf 'base='; git rev-parse "$base"
git merge-base --is-ancestor "$base" "$head" && echo 'base-is-ancestor=yes'

printf '%s\n' '== Pull request and exact-head checks =='
gh pr view "$pr" --repo "$repo" \
  --json headRefOid,baseRefOid,state,isDraft,mergeStateStatus,reviewDecision,statusCheckRollup,reviews \
  --jq '{headRefOid,baseRefOid,state,isDraft,mergeStateStatus,reviewDecision,checks:[.statusCheckRollup[]|select(.name != null)|{name,status,conclusion,headSha}],reviews:[.reviews[]|{author:.author.login,state,commit:.commit.oid}]}'
gh api "repos/$repo/commits/$head/check-runs?per_page=100" \
  --jq '[.check_runs[]|{name,status,conclusion,head_sha}]'

printf '%s\n' '== Unresolved review threads =='
gh api graphql \
  -F owner='ContextualWisdomLab' -F name='inkspan' -F number="$pr" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) { nodes { isResolved } }
    }
  }
}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length'

printf '%s\n' '== Diff inventory and hygiene =='
git diff --stat "$base" "$head"
git diff --name-status "$base" "$head"
git diff --check "$base" "$head" || true

printf '%s\n' '== Changed TypeScript outlines =='
while IFS= read -r f; do
  case "$f" in
    *.ts|*.tsx)
      printf '\n--- %s ---\n' "$f"
      ast-grep outline "$f" --items all --type function,class,interface,type 2>/dev/null || true
      ;;
  esac
done < <(git diff --name-only "$base" "$head")

printf '%s\n' '== Focused clipboard implementation and repair diff =='
ast-grep outline src/extensions/SafeClipboard.ts --items all || true
git diff --unified=100 "$base" "$head" -- \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeClipboard.test.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.coverageBranches.test.ts \
  src/extensions/SafeClipboard.ambientDom.test.ts \
  docs/clipboard-security.md \
  docs/doctoring/safe-rich-clipboard.md \
  CHANGELOG.md

printf '%s\n' '== Relevant sanitizer references and test declarations =='
rg -n -C 8 --glob '*.{ts,tsx,md}' \
  '(progress|meter|noframes|noembed|mso-hide|title|details|dialog|transformPastedHTML|onClipboardError)' \
  src docs CHANGELOG.md || true

Length of output: 12988


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='66cf4b9e46b5cbbc936ba470da5d1e96ca1649af'
head='75e96ebc2a853f9315eb166b8eb98264109b238e'

printf '%s\n' '== SafeClipboard structure =='
ast-grep outline src/extensions/SafeClipboard.ts --items all || true

printf '%s\n' '== SafeClipboard implementation =='
sed -n '1,240p' src/extensions/SafeClipboard.ts
sed -n '241,480p' src/extensions/SafeClipboard.ts
sed -n '481,760p' src/extensions/SafeClipboard.ts

printf '%s\n' '== New native-widget and obsolete-fallback repair =='
git diff --unified=100 "$head^" "$head" -- \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.test.ts \
  docs/clipboard-security.md \
  docs/doctoring/native-widget-fallback-content.md \
  docs/doctoring/safe-rich-clipboard.md \
  CHANGELOG.md

printf '%s\n' '== Focused sanitizer tests =='
rg -n -C 20 --glob '*.{test,spec}.{ts,tsx}' \
  '(progress|meter|noframes|noembed|mso-hide|title|details|dialog|node limit|depth limit|byte limit)' \
  src/extensions

printf '%s\n' '== Editor integration and extension assembly =='
cat -n src/extensions/kit.ts
cat -n src/components/CwlEditor.tsx
cat -n src/collaboration/CollaborativeCwlEditor.tsx
cat -n src/index.ts
cat -n src/types.ts

printf '%s\n' '== Clipboard integration test declarations =='
rg -n -C 10 --glob '*.{test,spec}.{ts,tsx}' \
  '(SafeClipboard|transformPastedHTML|onClipboardError|clipboard)' \
  src/components/CwlEditor.clipboard.test.tsx \
  src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx \
  src/extensions/kit.test.ts

printf '%s\n' '== Full-diff safety checks =='
git diff --unified=0 "$base" "$head" | rg -n -i \
  '^\+.*(-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----|api[_-]?key\s*[:=]|secret\s*[:=]|password\s*[:=]|authorization:\s*bearer)' \
  || true
git diff --summary "$base" "$head"

Length of output: 132491


@seonghobae I reviewed exact head 75e96ebc2a853f9315eb166b8eb98264109b238e against base 66cf4b9e46b5cbbc936ba470da5d1e96ca1649af.

I found no actionable current-head source-level finding.

The review verified these items:

  • SafeClipboard adds progress, meter, noframes, and noembed to DROPPED_SUBTREES.
  • The iterative traversal drops each complete subtree before it can reconstruct descendant fallback text.
  • The regression test verifies that all four fallback-text forms are absent while ordinary visible content remains.
  • The sanitizer retains its bounded byte, node, and depth limits.
  • The sanitizer reconstructs allowed content through its semantic and attribute allowlists.
  • The sanitizer keeps the prior title-metadata, hidden-subtree, closed-dialog, closed-details, and Office mso-hide: all boundaries.
  • The standalone and collaborative editor surfaces use the same sanitizer and the latest onClipboardError callback.
  • The exact-head checks already completed are successful or intentionally skipped.
  • The unresolved inline-thread count is zero.

I cannot approve this head yet. The exact-head strix check is still in_progress. GitHub reports mergeStateStatus: BLOCKED.

A new exact-head verification is required after strix completes successfully.

You are interacting with an AI system.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact current head 75e96ebc2a853f9315eb166b8eb98264109b238e. Treat prior rate-limit notices, predecessor-head findings, and explicitly superseded findings as non-evidence; report only still-valid actionable findings on this head.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='ContextualWisdomLab/inkspan'
pr='65'
base='66cf4b9e46b5cbbc936ba470da5d1e96ca1649af'
head='75e96ebc2a853f9315eb166b8eb98264109b238e'

printf '%s\n' '== Revision and pull-request state =='
printf 'checked-out='; git rev-parse HEAD
printf 'requested='; git rev-parse "$head"
printf 'base='; git rev-parse "$base"
git merge-base --is-ancestor "$base" "$head" && echo 'base-is-ancestor=yes'
gh pr view "$pr" --repo "$repo" \
  --json headRefOid,baseRefOid,state,isDraft,mergeStateStatus,reviewDecision,statusCheckRollup \
  --jq '{headRefOid,baseRefOid,state,isDraft,mergeStateStatus,reviewDecision,checks:[.statusCheckRollup[]|select(.name != null)|{name,status,conclusion,headSha}]}'

printf '%s\n' '== Exact-head check runs =='
gh api "repos/$repo/commits/$head/check-runs?per_page=100" \
  --jq '[.check_runs[]|{name,status,conclusion,head_sha}]'

printf '%s\n' '== Unresolved inline review threads =='
gh api graphql \
  -F owner='ContextualWisdomLab' -F name='inkspan' -F number="$pr" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) { nodes { isResolved } }
    }
  }
}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length'

printf '%s\n' '== Full diff inventory and hygiene =='
git diff --stat "$base" "$head"
git diff --name-status "$base" "$head"
git diff --check "$base" "$head" || true

printf '%s\n' '== Changed TypeScript structure =='
while IFS= read -r f; do
  case "$f" in
    *.ts|*.tsx)
      printf '\n--- %s ---\n' "$f"
      ast-grep outline "$f" --items all --type function,class,interface,type 2>/dev/null || true
      ;;
  esac
done < <(git diff --name-only "$base" "$head")

printf '%s\n' '== Sanitizer and editor implementation =='
for f in \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeLink.ts \
  src/extensions/kit.ts \
  src/components/CwlEditor.tsx \
  src/collaboration/CollaborativeCwlEditor.tsx \
  src/index.ts \
  src/types.ts; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '== Sanitizer and integration tests =='
for f in \
  src/extensions/SafeClipboard.test.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.coverageBranches.test.ts \
  src/extensions/SafeClipboard.ambientDom.test.ts \
  src/components/CwlEditor.clipboard.test.tsx \
  src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx \
  src/extensions/kit.test.ts; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '== Relevant direct references =='
rg -n -C 6 --glob '*.{ts,tsx}' \
  '(SafeClipboard|transformPastedHTML|onClipboardError|mso-hide|progress|meter|noframes|noembed|title|details|dialog)' \
  src || true

Length of output: 12754


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='66cf4b9e46b5cbbc936ba470da5d1e96ca1649af'
head='75e96ebc2a853f9315eb166b8eb98264109b238e'

printf '%s\n' '== SafeClipboard structure and implementation =='
ast-grep outline src/extensions/SafeClipboard.ts --items all || true
cat -n src/extensions/SafeClipboard.ts

printf '%s\n' '== SafeClipboard tests =='
for f in \
  src/extensions/SafeClipboard.test.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.coverageBranches.test.ts \
  src/extensions/SafeClipboard.ambientDom.test.ts; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '== Extension assembly and editor integration =='
for f in \
  src/extensions/SafeLink.ts \
  src/extensions/kit.ts \
  src/components/CwlEditor.tsx \
  src/collaboration/CollaborativeCwlEditor.tsx \
  src/index.ts \
  src/types.ts; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '== Integration tests =='
for f in \
  src/extensions/kit.test.ts \
  src/components/CwlEditor.clipboard.test.tsx \
  src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '== Runtime source diff =='
git diff --unified=100 "$base" "$head" -- \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeLink.ts \
  src/extensions/kit.ts \
  src/components/CwlEditor.tsx \
  src/collaboration/CollaborativeCwlEditor.tsx \
  src/index.ts \
  src/types.ts

printf '%s\n' '== All transform ordering and sanitizer configuration sites =='
rg -n -C 8 --glob '*.{ts,tsx}' \
  '(transformPastedHTML|SafeClipboard\.configure|SafeClipboard|onClipboardError|priority\s*:)' \
  src

Length of output: 240916


@seonghobae I reviewed exact head 75e96ebc2a853f9315eb166b8eb98264109b238e against base 66cf4b9e46b5cbbc936ba470da5d1e96ca1649af.

I found no actionable current-head source-level finding.

The review verified the bounded reconstruction allowlist, dropped-subtree handling, mso-hide: all parsing, metadata removal, native-widget and obsolete-fallback removal, closed details and dialog handling, stable redacted errors, callback liveness, and both editor integrations.

The requested commit remains the current pull-request head. The pull request has zero unresolved inline threads.

I cannot issue approval for this head yet. The exact-head strix check is still in progress. GitHub reports mergeStateStatus: BLOCKED.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@opencode-agent review
@cwl-noema-review review
@strix review

Perform read-only independent review of exact current head 75e96ebc2a853f9315eb166b8eb98264109b238e against base 66cf4b9e46b5cbbc936ba470da5d1e96ca1649af. Do not write to the branch, reuse predecessor-head evidence, or submit approval unless the exact current head has no actionable finding and repository policy permits it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/extensions/SafeClipboard.ts (1)

534-548: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

불안전 링크 분기를 단순화하세요.

539-541행의 childParent = frame.outputParent 재대입은 무연산입니다. childParent는 535행에서 이미 같은 값입니다. 또한 같은 isSafeLinkHref 검사가 copyAllowedAttributes(401행)에서 다시 실행됩니다. 조건을 한 번만 계산하면 두 지점의 정책이 갈라질 위험이 사라집니다.

♻️ 제안 리팩터
       const outputName = normalizedOutputElement(sourceName);
+      const unwrapUnsafeLink =
+        outputName === 'a' &&
+        !isSafeLinkHref(sourceElement.getAttribute('href'));
       let childParent = frame.outputParent;
-      if (outputName !== null) {
-        if (
-          outputName === 'a' &&
-          !isSafeLinkHref(sourceElement.getAttribute('href'))
-        ) {
-          childParent = frame.outputParent;
-        } else {
-          const outputElement = inertDocument.createElement(outputName);
-          copyAllowedAttributes(sourceElement, outputElement);
-          frame.outputParent.appendChild(outputElement);
-          childParent = outputElement;
-        }
+      if (outputName !== null && !unwrapUnsafeLink) {
+        const outputElement = inertDocument.createElement(outputName);
+        copyAllowedAttributes(sourceElement, outputElement);
+        frame.outputParent.appendChild(outputElement);
+        childParent = outputElement;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/extensions/SafeClipboard.ts` around lines 534 - 548, In the
output-element handling around normalizedOutputElement, remove the redundant
unsafe-link branch that reassigns childParent to frame.outputParent, and compute
the isSafeLinkHref result once for reuse with copyAllowedAttributes. Preserve
the existing behavior of skipping unsafe anchor creation while ensuring the same
safety decision is used consistently.
src/types.ts (1)

272-281: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

clipboard의 적용 시점을 주석에 명시하세요.

clipboard 정책은 편집기 생성 시 buildExtensions()에 전달됩니다. 편집기 생성 후 새 clipboard 객체로 교체해도 기존 확장의 정책은 변경되지 않습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types.ts` around lines 272 - 281, Update the documentation comment for
the clipboard property in the editor configuration type to state that the policy
is applied when the editor is created through buildExtensions() and that
replacing the clipboard object afterward does not update existing extensions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/doctoring/closed-interactive-content.md`:
- Around line 99-100: Update the date metadata in
docs/doctoring/closed-interactive-content.md lines 99-100 by changing or
removing “Retrieved August 6, 2026” so it is not later than the August 5, 2026
review date; also update “Decision date: 2026-08-06” in
docs/doctoring/native-widget-fallback-content.md lines 3-4 to the actual
decision date of 2026-08-05.

In `@docs/doctoring/safe-rich-clipboard.md`:
- Around line 127-141: Update the “Dropped subtrees and privacy boundary”
section to include or clearly cross-reference the established rules for removing
progress, meter, noframes, and noembed elements; handling closed details and
dialog elements; and parsing CSS escapes and EOF-terminated comments in Office
hidden-content detection. Keep the existing removal and mso-hide behavior intact
while synchronizing this document with the related doctoring records.
- Around line 162-165: Update the memory-behavior statement in the documentation
near “Traversal is iterative” to remove the claim that parsing allocates at most
the bounded input representation. Describe only the enforced UTF-8 input, node,
and depth limits, and acknowledge that parsed and output trees are additionally
allocated without claiming an explicit heap or DOM memory cap.

In `@docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md`:
- Around line 121-134: Update the defect count in the introductory statement to
match the four items listed under the checklist, or regroup the checklist so it
contains three implementation defects and one assurance gap. Keep the
Configuration boundary, Office hidden content, Transform ordering, and Incorrect
test assertion entries accurately represented for review traceability.

In `@src/extensions/SafeClipboard.ts`:
- Around line 10-11: Validate DEFAULT_CLIPBOARD_MAX_NODES against the
repository’s representative Word HTML fixture by measuring its traversed node
count. If the fixture exceeds the default, either raise the default to cover it
or update transformPastedHTML to provide an explicit plain-text fallback while
preserving the existing behavior for smaller inputs.

---

Nitpick comments:
In `@src/extensions/SafeClipboard.ts`:
- Around line 534-548: In the output-element handling around
normalizedOutputElement, remove the redundant unsafe-link branch that reassigns
childParent to frame.outputParent, and compute the isSafeLinkHref result once
for reuse with copyAllowedAttributes. Preserve the existing behavior of skipping
unsafe anchor creation while ensuring the same safety decision is used
consistently.

In `@src/types.ts`:
- Around line 272-281: Update the documentation comment for the clipboard
property in the editor configuration type to state that the policy is applied
when the editor is created through buildExtensions() and that replacing the
clipboard object afterward does not update existing extensions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 14587e8c-8214-472e-a0e6-c65e5fd56324

📥 Commits

Reviewing files that changed from the base of the PR and between 66cf4b9 and ec5d602.

📒 Files selected for processing (26)
  • .github/workflows/ci.yml
  • ARCHITECTURE.md
  • CHANGELOG.md
  • docs/clipboard-security.md
  • docs/doctoring/closed-interactive-content.md
  • docs/doctoring/css-escaped-office-hidden-content.md
  • docs/doctoring/native-widget-fallback-content.md
  • docs/doctoring/safe-rich-clipboard.md
  • docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md
  • docs/superpowers/specs/2026-08-05-safe-rich-clipboard-design.md
  • src/clipboardDocumentation.test.ts
  • src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx
  • src/collaboration/CollaborativeCwlEditor.tsx
  • src/components/CwlEditor.clipboard.test.tsx
  • src/components/CwlEditor.tsx
  • src/extensions/SafeClipboard.ambientDom.test.ts
  • src/extensions/SafeClipboard.coverageBranches.test.ts
  • src/extensions/SafeClipboard.datalistRegression.test.ts
  • src/extensions/SafeClipboard.securityRegression.test.ts
  • src/extensions/SafeClipboard.test.ts
  • src/extensions/SafeClipboard.ts
  • src/extensions/kit.test.ts
  • src/extensions/kit.ts
  • src/index.ts
  • src/types.ts
  • src/workflowExactHead.test.ts

Comment thread docs/doctoring/closed-interactive-content.md Outdated
Comment thread docs/doctoring/safe-rich-clipboard.md Outdated
Comment thread docs/doctoring/safe-rich-clipboard.md Outdated
Comment thread docs/superpowers/plans/2026-08-05-safe-rich-clipboard.md Outdated
Comment thread src/extensions/SafeClipboard.ts

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact head 95dc3eb8a7d40f049859d94a8019c594607db612. Exact-head CI 31058980885, Security Scan 31058980977, and SAST Semgrep 31058980840 all succeeded; production statement/branch/function/line coverage is 100%, and unresolved inline review threads are 0. The previous exact-stack attempt was infrastructure-only rate limiting and is not treated as approval or success evidence.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo='ContextualWisdomLab/inkspan'
pr='65'
head='95dc3eb8a7d40f049859d94a8019c594607db612'

printf '%s\n' '== Requested revision and pull request state =='
printf 'checked-out='; git rev-parse HEAD
printf 'requested='; git rev-parse "$head"
gh pr view "$pr" --repo "$repo" \
  --json headRefOid,baseRefOid,state,isDraft,mergeStateStatus,reviewDecision,statusCheckRollup \
  --jq '{headRefOid,baseRefOid,state,isDraft,mergeStateStatus,reviewDecision,checks:[.statusCheckRollup[]|select(.name != null)|{name,status,conclusion,headSha}]}'

base="$(gh pr view "$pr" --repo "$repo" --json baseRefOid --jq '.baseRefOid')"
printf 'base=%s\n' "$base"
git merge-base --is-ancestor "$base" "$head" && echo 'base-is-ancestor=yes'

printf '%s\n' '== Exact-head check runs and unresolved threads =='
gh api "repos/$repo/commits/$head/check-runs?per_page=100" \
  --jq '[.check_runs[]|{name,status,conclusion,head_sha}]'
gh api graphql \
  -F owner='ContextualWisdomLab' -F name='inkspan' -F number="$pr" \
  -f query='
query($owner: String!, $name: String!, $number: Int!) {
  repository(owner: $owner, name: $name) {
    pullRequest(number: $number) {
      reviewThreads(first: 100) { nodes { isResolved } }
    }
  }
}' --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)] | length'

printf '%s\n' '== Complete current-head diff inventory =='
git diff --stat "$base" "$head"
git diff --name-status "$base" "$head"
git diff --check "$base" "$head" || true

printf '%s\n' '== Changed TypeScript structure =='
while IFS= read -r f; do
  case "$f" in
    *.ts|*.tsx)
      printf '\n--- %s ---\n' "$f"
      ast-grep outline "$f" --items all --type function,class,interface,type 2>/dev/null || true
      ;;
  esac
done < <(git diff --name-only "$base" "$head")

printf '%s\n' '== Sanitizer, extension assembly, and editor surfaces =='
for f in \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeLink.ts \
  src/extensions/kit.ts \
  src/components/CwlEditor.tsx \
  src/collaboration/CollaborativeCwlEditor.tsx \
  src/index.ts \
  src/types.ts; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '== Sanitizer and integration tests =='
for f in \
  src/extensions/SafeClipboard.test.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.coverageBranches.test.ts \
  src/extensions/SafeClipboard.ambientDom.test.ts \
  src/extensions/SafeClipboard.datalistRegression.test.ts \
  src/components/CwlEditor.clipboard.test.tsx \
  src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx \
  src/extensions/kit.test.ts; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    cat -n "$f"
  fi
done

printf '%s\n' '== Direct boundary, transform, and callback references =='
rg -n -C 6 --glob '*.{ts,tsx}' \
  '(SafeClipboard|transformPastedHTML|onClipboardError|mso-hide|progress|meter|noframes|noembed|datalist|title|details|dialog|priority\s*:)' \
  src || true

Length of output: 13690


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base='66cf4b9e46b5cbbc936ba470da5d1e96ca1649af'
prior='75e96ebc2a853f9315eb166b8eb98264109b238e'
head='95dc3eb8a7d40f049859d94a8019c594607db612'

printf '%s\n' '== Incremental changes after the prior reviewed revision =='
git merge-base --is-ancestor "$prior" "$head" && echo 'prior-is-ancestor=yes' || echo 'prior-is-ancestor=no'
git diff --stat "$prior" "$head"
git diff --name-status "$prior" "$head"
git diff --unified=100 "$prior" "$head" -- \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.datalistRegression.test.ts \
  src/extensions/SafeClipboard.wordCapacity.test.ts \
  src/datalistDocumentation.test.ts \
  src/clipboardDocumentation.test.ts \
  docs/clipboard-security.md \
  docs/doctoring/datalist-hidden-suggestion-content.md \
  docs/doctoring/visibility-collapse-hidden-content.md \
  CHANGELOG.md

printf '%s\n' '== SafeClipboard structure and implementation =='
ast-grep outline src/extensions/SafeClipboard.ts --items all || true
cat -n src/extensions/SafeClipboard.ts

printf '%s\n' '== Direct sanitizer visibility and traversal paths =='
rg -n -C 12 --glob '*.{ts,tsx}' \
  '(visibility|collapse|hidden|datalist|option|DROPPED_SUBTREES|isElementHidden|sanitizeRichClipboardHtml|walk|nodeCount|maxDepth)' \
  src/extensions/SafeClipboard.ts src/extensions/SafeClipboard*.test.ts

printf '%s\n' '== Focused sanitizer tests =='
for f in \
  src/extensions/SafeClipboard.test.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.coverageBranches.test.ts \
  src/extensions/SafeClipboard.ambientDom.test.ts \
  src/extensions/SafeClipboard.datalistRegression.test.ts \
  src/extensions/SafeClipboard.wordCapacity.test.ts; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '== Extension assembly and both editor integrations =='
for f in \
  src/extensions/SafeLink.ts \
  src/extensions/kit.ts \
  src/components/CwlEditor.tsx \
  src/collaboration/CollaborativeCwlEditor.tsx \
  src/types.ts \
  src/index.ts; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '== Editor and extension integration tests =='
for f in \
  src/extensions/kit.test.ts \
  src/components/CwlEditor.clipboard.test.tsx \
  src/collaboration/CollaborativeCwlEditor.clipboard.test.tsx; do
  printf '\n--- %s ---\n' "$f"
  cat -n "$f"
done

printf '%s\n' '== All extension transform implementations and ordering sites =='
rg -n -C 8 --glob '*.{ts,tsx}' \
  '(transformPastedHTML|priority\s*:|buildExtensions\s*\(|SafeClipboard\.configure)' \
  src

Length of output: 120853


🏁 Script executed:

#!/bin/bash
set -euo pipefail

prior='75e96ebc2a853f9315eb166b8eb98264109b238e'
head='95dc3eb8a7d40f049859d94a8019c594607db612'

printf '%s\n' '== Incremental runtime and regression-test diff =='
git diff --unified=40 "$prior" "$head" -- \
  src/extensions/SafeClipboard.ts \
  src/extensions/SafeClipboard.securityRegression.test.ts \
  src/extensions/SafeClipboard.datalistRegression.test.ts \
  src/extensions/SafeClipboard.wordCapacity.test.ts

printf '%s\n' '== SafeClipboard implementation: declarations, limits, and style parsing =='
sed -n '1,230p' src/extensions/SafeClipboard.ts

printf '%s\n' '== SafeClipboard implementation: visibility and reconstruction =='
sed -n '231,460p' src/extensions/SafeClipboard.ts

printf '%s\n' '== SafeClipboard implementation: traversal and TipTap boundary =='
sed -n '461,660p' src/extensions/SafeClipboard.ts

printf '%s\n' '== Focused visibility regression tests =='
sed -n '1,230p' src/extensions/SafeClipboard.securityRegression.test.ts
cat -n src/extensions/SafeClipboard.datalistRegression.test.ts
cat -n src/extensions/SafeClipboard.wordCapacity.test.ts

Length of output: 54175


@seonghobae I reviewed exact head 95dc3eb8a7d40f049859d94a8019c594607db612 against base 66cf4b9e46b5cbbc936ba470da5d1e96ca1649af.

I found no actionable current-head source-level finding.

The review verified these items:

  • SafeClipboard uses bounded byte, node, and depth limits.
  • The sanitizer reconstructs HTML through strict element and attribute allowlists.
  • Unsafe links are unwrapped. Safe links preserve only the approved href and fixed rel.
  • Hidden content includes hidden, aria-hidden, display: none, visibility: hidden, visibility: collapse, and Office mso-hide: all.
  • datalist and option subtrees are removed before their fallback text can enter editor content.
  • Native-widget, metadata, resource, embedded, interactive, and unsupported subtrees remain removed.
  • The sanitizer preserves the fail-closed error boundary and avoids evaluating hostile configuration accessors during editor creation.
  • Both editor surfaces use the latest onClipboardError callback without editor or Yjs-binding recreation.
  • SafeClipboard remains the final ordinary transformPastedHTML transform in the tested extension set.
  • The new Word-like capacity and link-attribute-read regressions are consistent with the implementation.

I cannot approve this head yet. The exact-head strix check is still in_progress. Pull request #65 remains BLOCKED. The unresolved inline review-thread count is zero.

A new exact-head status verification is required after strix completes successfully.

You are interacting with an AI system.

@seonghobae seonghobae left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head security review for 95dc3eb8a7d40f049859d94a8019c594607db612 found one valid hidden-content gap. The finding is recorded now but must be repaired only after prerequisite PR #64 reaches main, so the SafeClipboard branch remains single-writer and is reconciled once against the authoritative architecture/changelog base. Do not resolve the thread before a test-first post-#64 fix and complete exact-head revalidation.

}
const style = (element as HTMLElement).style;
const visibility = style.visibility.trim().toLowerCase();
return (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid hidden-content boundary gap: content-visibility: hidden is not recognized here, so its source-only descendants are unwrapped into ordinary editor prose after the style attribute is discarded. CSS Containment Module Level 2 specifies that content-visibility: hidden skips the element’s contents and excludes them from user-agent features similarly to display: none (W3C, CSS Containment Module Level 2, §4). This can expose text that was neither rendered nor available to accessibility/find/selection surfaces in the source.

Preserve dependency order: do not write this branch until PR #64 is integrated. On the exact post-#64 base, add the failing regression first, covering ordinary/case/whitespace/terminal-!important, CSS-comment and CSS-escaped property/value forms, plus false positives (visible, auto, hiddenly, prefixed property names). Then extend the bounded raw-style hidden-declaration path so the complete subtree is dropped without relying on engine-specific CSSOM exposure. Update operator guidance, APA 7 doctoring, documentation contracts, and CHANGELOG.md; restore 100% statement/branch/function/line coverage and every exact-head gate before resolving this thread.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant